//
//  Copyright (c) 2012 Artyom Beilis (Tonkikh)
//
//  Distributed under the Boost Software License, Version 1.0. (See
//  accompanying file LICENSE_1_0.txt or copy at
//  http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef NOWIDE_FILEBUF_HPP
#    define NOWIDE_FILEBUF_HPP

#    include <fstream>
#    include <iosfwd>
#    include <nowide/config.hpp>
#    include <nowide/stackstring.hpp>
#    include <stdio.h>
#    include <streambuf>

#    ifdef NOWIDE_MSVC
#        pragma warning(push)
#        pragma warning(disable : 4996 4242 4244 4800)
#    endif

namespace nowide
{
#    if !defined(NOWIDE_WINDOWS) && !defined(NOWIDE_FSTREAM_TESTS) && !defined(NOWIDE_DOXYGEN)
    using std::basic_filebuf;
    using std::filebuf;
#    else    // Windows

    ///
    /// \brief This forward declaration defined the basic_filebuf type.
    ///
    /// it is implemented and specialized for CharType = char, it behaves
    /// implements std::filebuf over standard C I/O
    ///
    template<typename CharType, typename Traits = std::char_traits<CharType>>
    class basic_filebuf;

    ///
    /// \brief This is implementation of std::filebuf
    ///
    /// it is implemented and specialized for CharType = char, it behaves
    /// implements std::filebuf over standard C I/O
    ///
    template<>
    class basic_filebuf<char> : public std::basic_streambuf<char>
    {
    public:
        ///
        /// Creates new filebuf
        ///
        basic_filebuf() : buffer_size_(4), buffer_(0), file_(0), own_(true), mode_(std::ios::in | std::ios::out)
        {
            setg(0, 0, 0);
            setp(0, 0);
        }

        virtual ~basic_filebuf()
        {
            if (file_) {
                ::fclose(file_);
                file_ = 0;
            }
            if (own_ && buffer_) delete[] buffer_;
        }

        ///
        /// Same as std::filebuf::open but s is UTF-8 string
        ///
        basic_filebuf* open(std::string const& s, std::ios_base::openmode mode)
        {
            return open(s.c_str(), mode);
        }
        ///
        /// Same as std::filebuf::open but s is UTF-8 string
        ///
        basic_filebuf* open(char const* s, std::ios_base::openmode mode)
        {
            if (file_) {
                sync();
                ::fclose(file_);
                file_ = 0;
            }
            bool ate = bool(mode & std::ios_base::ate);
            if (ate) mode = mode ^ std::ios_base::ate;
            wchar_t const* smode = get_mode(mode);
            if (!smode) return 0;
            wstackstring name;
            if (!name.convert(s)) return 0;
#        ifdef NOWIDE_FSTREAM_TESTS
            FILE* f = ::fopen(s, nowide::convert(smode).c_str());
#        else
            FILE* f = ::_wfopen(name.c_str(), smode);
#        endif
            if (!f) return 0;
            if (ate && fseek(f, 0, SEEK_END) != 0) {
                fclose(f);
                return 0;
            }
            file_ = f;
            return this;
        }
        ///
        /// Same as std::filebuf::close()
        ///
        basic_filebuf* close()
        {
            bool res = sync() == 0;
            if (file_) {
                if (::fclose(file_) != 0) res = false;
                file_ = 0;
            }
            return res ? this : 0;
        }
        ///
        /// Same as std::filebuf::is_open()
        ///
        bool is_open() const
        {
            return file_ != 0;
        }

    private:
        void make_buffer()
        {
            if (buffer_) return;
            if (buffer_size_ > 0) {
                buffer_ = new char[buffer_size_];
                own_    = true;
            }
        }

    protected:
        virtual std::streambuf* setbuf(char* s, std::streamsize n)
        {
            if (!buffer_ && n >= 0) {
                buffer_      = s;
                buffer_size_ = n;
                own_         = false;
            }
            return this;
        }

#        ifdef NOWIDE_DEBUG_FILEBUF

        void print_buf(char* b, char* p, char* e)
        {
            std::cerr << "-- Is Null: " << (b == 0) << std::endl;
            ;
            if (b == 0) return;
            if (e != 0)
                std::cerr << "-- Total: " << e - b << " offset from start " << p - b << std::endl;
            else
                std::cerr << "-- Total: " << p - b << std::endl;

            std::cerr << "-- [";
            for (char* ptr = b; ptr < p; ptr++) std::cerr << *ptr;
            if (e != 0) {
                std::cerr << "|";
                for (char* ptr = p; ptr < e; ptr++) std::cerr << *ptr;
            }
            std::cerr << "]" << std::endl;
        }

        void print_state()
        {
            std::cerr << "- Output:" << std::endl;
            print_buf(pbase(), pptr(), 0);
            std::cerr << "- Input:" << std::endl;
            print_buf(eback(), gptr(), egptr());
            std::cerr << "- fpos: " << (file_ ? ftell(file_) : -1L) << std::endl;
        }

        struct print_guard
        {
            print_guard(basic_filebuf* p, char const* func)
            {
                self = p;
                f    = func;
                std::cerr << "In: " << f << std::endl;
                self->print_state();
            }
            ~print_guard()
            {
                std::cerr << "Out: " << f << std::endl;
                self->print_state();
            }
            basic_filebuf* self;
            char const*    f;
        };
#        else
#        endif

        int overflow(int c)
        {
#        ifdef NOWIDE_DEBUG_FILEBUF
            print_guard g(this, __FUNCTION__);
#        endif
            if (!file_) return EOF;

            if (fixg() < 0) return EOF;

            size_t n = pptr() - pbase();
            if (n > 0) {
                if (::fwrite(pbase(), 1, n, file_) < n) return -1;
                fflush(file_);
            }

            if (buffer_size_ > 0) {
                make_buffer();
                setp(buffer_, buffer_ + buffer_size_);
                if (c != EOF) sputc(c);
            }
            else if (c != EOF) {
                if (::fputc(c, file_) == EOF) return EOF;
                fflush(file_);
            }
            return 0;
        }

        int sync()
        {
            return overflow(EOF);
        }

        int underflow()
        {
#        ifdef NOWIDE_DEBUG_FILEBUF
            print_guard g(this, __FUNCTION__);
#        endif
            if (!file_) return EOF;
            if (fixp() < 0) return EOF;
            if (buffer_size_ == 0) {
                int c = ::fgetc(file_);
                if (c == EOF) {
                    return EOF;
                }
                last_char_ = c;
                setg(&last_char_, &last_char_, &last_char_ + 1);
                return c;
            }
            make_buffer();
            size_t n = ::fread(buffer_, 1, buffer_size_, file_);
            setg(buffer_, buffer_, buffer_ + n);
            if (n == 0) return EOF;
            return std::char_traits<char>::to_int_type(*gptr());
        }

        int pbackfail(int)
        {
            return pubseekoff(-1, std::ios::cur);
        }

        std::streampos seekoff(std::streamoff off, std::ios_base::seekdir seekdir, std::ios_base::openmode /*m*/)
        {
#        ifdef NOWIDE_DEBUG_FILEBUF
            print_guard g(this, __FUNCTION__);
#        endif
            if (!file_) return EOF;
            if (fixp() < 0 || fixg() < 0) return EOF;
            if (seekdir == std::ios_base::cur) {
                if (::fseek(file_, off, SEEK_CUR) < 0) return EOF;
            }
            else if (seekdir == std::ios_base::beg) {
                if (::fseek(file_, off, SEEK_SET) < 0) return EOF;
            }
            else if (seekdir == std::ios_base::end) {
                if (::fseek(file_, off, SEEK_END) < 0) return EOF;
            }
            else
                return -1;
            return ftell(file_);
        }
        std::streampos seekpos(std::streampos off, std::ios_base::openmode m)
        {
            return seekoff(std::streamoff(off), std::ios_base::beg, m);
        }

    private:
        int fixg()
        {
            if (gptr() != egptr()) {
                std::streamsize off = gptr() - egptr();
                setg(0, 0, 0);
                if (fseek(file_, off, SEEK_CUR) != 0) return -1;
            }
            setg(0, 0, 0);
            return 0;
        }

        int fixp()
        {
            if (pptr() != 0) {
                int r = sync();
                setp(0, 0);
                return r;
            }
            return 0;
        }

        void reset(FILE* f = 0)
        {
            sync();
            if (file_) {
                fclose(file_);
                file_ = 0;
            }
            file_ = f;
        }

        static wchar_t const* get_mode(std::ios_base::openmode mode)
        {
            //
            // done according to n2914 table 106 27.9.1.4
            //

            // note can't use switch case as overload operator can't be used
            // in constant expression
            if (mode == (std::ios_base::out)) return L"w";
            if (mode == (std::ios_base::out | std::ios_base::app)) return L"a";
            if (mode == (std::ios_base::app)) return L"a";
            if (mode == (std::ios_base::out | std::ios_base::trunc)) return L"w";
            if (mode == (std::ios_base::in)) return L"r";
            if (mode == (std::ios_base::in | std::ios_base::out)) return L"r+";
            if (mode == (std::ios_base::in | std::ios_base::out | std::ios_base::trunc)) return L"w+";
            if (mode == (std::ios_base::in | std::ios_base::out | std::ios_base::app)) return L"a+";
            if (mode == (std::ios_base::in | std::ios_base::app)) return L"a+";
            if (mode == (std::ios_base::binary | std::ios_base::out)) return L"wb";
            if (mode == (std::ios_base::binary | std::ios_base::out | std::ios_base::app)) return L"ab";
            if (mode == (std::ios_base::binary | std::ios_base::app)) return L"ab";
            if (mode == (std::ios_base::binary | std::ios_base::out | std::ios_base::trunc)) return L"wb";
            if (mode == (std::ios_base::binary | std::ios_base::in)) return L"rb";
            if (mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::out)) return L"r+b";
            if (mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::out | std::ios_base::trunc)) return L"w+b";
            if (mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::out | std::ios_base::app)) return L"a+b";
            if (mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::app)) return L"a+b";
            return 0;
        }

        size_t             buffer_size_;
        char*              buffer_;
        FILE*              file_;
        bool               own_;
        char               last_char_;
        std::ios::openmode mode_;
    };

    ///
    /// \brief Convinience typedef
    ///
    typedef basic_filebuf<char> filebuf;

#    endif    // windows

}    // namespace nowide

#    ifdef NOWIDE_MSVC
#        pragma warning(pop)
#    endif

#endif

// vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
